/*
 * Copyright (C) 2008 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.tools.perflib.heap;

import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.tools.perflib.heap.io.HprofBuffer;
import com.google.common.collect.Sets;
import com.google.common.primitives.UnsignedBytes;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Set;

public abstract class Instance {

    protected final long mId;

    //  The stack in which this object was allocated
    @NonNull
    protected final StackTrace mStack;

    //  Id of the ClassObj of which this object is an instance
    long mClassId;

    //  The heap in which this object was allocated (app, zygote, etc)
    Heap mHeap;

    //  The size of this object
    int mSize;

    //  Another identifier for this Instance, that we computed during the analysis phase.
    int mTopologicalOrder;

    //  The immediate dominator of this instance, or null if not reachable from any GC roots.
    @Nullable
    private Instance mImmediateDominator;

    //  The retained size of this object, indexed by heap (default, image, app, zygote).
    //  Intuitively, this represents the amount of memory that could be reclaimed in each heap if
    //  the instance were removed.
    //  To save space, we only keep a primitive array here following the order in mSnapshot.mHeaps.
    private long[] mRetainedSizes;

    //  List of all objects that hold a live reference to this object
    private final ArrayList<Instance> mReferences = new ArrayList<Instance>();

    Instance(long id, @NonNull StackTrace stackTrace) {
        mId = id;
        mStack = stackTrace;
    }

    public long getId() {
        return mId;
    }

    public abstract void accept(Visitor visitor);

    public void setClassId(long classId) {
        mClassId = classId;
    }

    public ClassObj getClassObj() {
        return mHeap.mSnapshot.findClass(mClassId);
    }

    public final int getCompositeSize() {
        CollectingVisitor visitor = new CollectingVisitor();
        this.accept(visitor);

        int size = 0;
        for (Instance instance : visitor.getVisited()) {
            size += instance.getSize();
        }
        return size;
    }

    //  Returns the instrinsic size of a given object
    public int getSize() {
        return mSize;
    }

    public void setSize(int size) {
        mSize = size;
    }

    public void setHeap(Heap heap) {
        mHeap = heap;
    }

    public Heap getHeap() {
        return mHeap;
    }

    public int getTopologicalOrder() {
        return mTopologicalOrder;
    }

    public void setTopologicalOrder(int topologicalOrder) {
        mTopologicalOrder = topologicalOrder;
    }

    @Nullable
    public Instance getImmediateDominator() {
        return mImmediateDominator;
    }

    public void setImmediateDominator(@NonNull Instance dominator) {
        mImmediateDominator = dominator;
    }

    public void resetRetainedSize() {
        List<Heap> allHeaps = mHeap.mSnapshot.mHeaps;
        if (mRetainedSizes == null) {
            mRetainedSizes = new long[allHeaps.size()];
        } else {
            Arrays.fill(mRetainedSizes, 0);
        }
        mRetainedSizes[allHeaps.indexOf(mHeap)] = getSize();
    }

    public void addRetainedSize(int heapIndex, long size) {
        mRetainedSizes[heapIndex] += size;
    }

    public long getRetainedSize(int heapIndex) {
        return mRetainedSizes[heapIndex];
    }

    //  Add to the list of objects that have a hard reference to this Instance
    public void addReference(Instance reference) {
        mReferences.add(reference);
    }

    @NonNull
    public ArrayList<Instance> getReferences() {
        return mReferences;
    }

    @Nullable
    protected Object readValue(@NonNull Type type) {
        switch (type) {
            case OBJECT:
                long id = readId();
                Instance result = mHeap.mSnapshot.findReference(id);
                if (result != null) {
                    result.addReference(this);
                }
                return result;
            case BOOLEAN:
                return getBuffer().readByte() != 0;
            case CHAR:
                return getBuffer().readChar();
            case FLOAT:
                return getBuffer().readFloat();
            case DOUBLE:
                return getBuffer().readDouble();
            case BYTE:
                return getBuffer().readByte();
            case SHORT:
                return getBuffer().readShort();
            case INT:
                return getBuffer().readInt();
            case LONG:
                return getBuffer().readLong();
        }
        return null;
    }

    protected long readId() {
        // As long as we don't interpret IDs, reading signed values here is fine.
        switch (Type.OBJECT.getSize()) {
            case 1:
                return getBuffer().readByte();
            case 2:
                return getBuffer().readShort();
            case 4:
                return getBuffer().readInt();
            case 8:
                return getBuffer().readLong();
        }
        return 0;
    }

    protected int readUnsignedByte(){
        return UnsignedBytes.toInt(getBuffer().readByte());
    }

    protected int readUnsignedShort() {
        return getBuffer().readShort() & 0xffff;
    }

    protected HprofBuffer getBuffer() {
        return mHeap.mSnapshot.mBuffer;
    }

    public static class CollectingVisitor implements Visitor {

        private final Set<Instance> mVisited = Sets.newHashSet();

        @Override
        public boolean visitEnter(Instance instance) {
            //  If we're in the set then we and our children have been visited
            return mVisited.add(instance);
        }

        @Override
        public void visitLeave(Instance instance) {
        }

        public Set<Instance> getVisited() {
            return mVisited;
        }
      }
}
